package zcrontab

import (
	"time"
)

type bounds struct {
	min   uint
	max   uint
	names map[string]uint
}

const (
	// 首bit为置为1,表示*
	starBit = 1 << 63
)

// The bounds for each field.
var (
	seconds = bounds{0, 59, nil}
	minutes = bounds{0, 59, nil}
	hours   = bounds{0, 23, nil}
	dom     = bounds{1, 31, nil}
	months  = bounds{1, 12, map[string]uint{
		"jan": 1,
		"feb": 2,
		"mar": 3,
		"apr": 4,
		"may": 5,
		"jun": 6,
		"jul": 7,
		"aug": 8,
		"sep": 9,
		"oct": 10,
		"nov": 11,
		"dec": 12,
	}}
	dow = bounds{0, 6, map[string]uint{
		"sun": 0,
		"mon": 1,
		"tue": 2,
		"wed": 3,
		"thu": 4,
		"fri": 5,
		"sat": 6,
	}}
)

// Schedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
type Schedule struct {
	Second, Minute, Hour, Dom, Month, Dow uint64

	// Override location for this schedule.
	Location *time.Location
}

func (s *Schedule) Next(t time.Time) time.Time {
	loc := t.Location()

	// 这段代码的作用是将当前时间加上一秒，并从中减去当前时间的纳秒值，以确保时间从下一秒的开始计算。
	// 例如，如果当前时间是12:34:56.789，那么这段代码将在其上加上一秒，得到12:34:57.789。
	// 然后，它将从中减去纳秒值（789），得到12:34:57.000。
	// 这样可以确保时间从指定的秒的开始计算，这对于需要在特定时间运行的任务非常有用。
	t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)

	yearLimit := t.Year() + 5

WRAP:
	if t.Year() > yearLimit {
		return time.Time{}
	}

	// 当前月份不符合时进入寻找相符的月份
	for 1<<uint(t.Month())&s.Month == 0 {
		t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc).AddDate(0, 1, 0)

		if t.Month() == time.January {
			goto WRAP
		}
	}

	for !dayMatches(s, t) {
		t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, 1)

		if t.Hour() != 0 {
			if t.Hour() > 12 {
				t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
			} else {
				t = t.Add(time.Duration(-t.Hour()) * time.Hour)
			}
		}

		if t.Day() == 1 {
			goto WRAP
		}
	}

	for 1<<uint(t.Hour())&s.Hour == 0 {
		t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc).Add(1 * time.Hour)

		if t.Hour() == 0 {
			goto WRAP
		}
	}

	for 1<<uint(t.Minute())&s.Minute == 0 {
		t = t.Truncate(time.Minute).Add(1 * time.Minute)

		if t.Minute() == 0 {
			goto WRAP
		}
	}

	for 1<<uint(t.Second())&s.Second == 0 {
		t = t.Truncate(time.Second).Add(1 * time.Second)

		if t.Second() == 0 {
			goto WRAP
		}
	}

	return t.In(t.Location())
}

func dayMatches(s *Schedule, t time.Time) bool {
	domMatch := 1<<uint(t.Day())&s.Dom > 0     // 命中月中的天
	dowMatch := 1<<uint(t.Weekday())&s.Dow > 0 // 命中周中的天

	if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
		return domMatch && dowMatch
	} else {
		return domMatch || dowMatch
	}
}
